#!python
# -*- coding: utf-8 -*-
"""
Curses-based rpio interface

Author: Chris Hager <chris@linuxuser.at>
URL: http://pythonhosted.org//RPIO
License: LGPLv3+
"""
import os
import sys

# Command line options include 'no_rpio' (use dummy data), 'no_thread'
# and 'dev' (which just sets no_rpio and no_thread)
CMD_OPTIONS = sys.argv[1:] if len(sys.argv) > 1 else []
if "dev" in CMD_OPTIONS:
    CMD_OPTIONS += ("no_rpio", "no_thread")

# Automatically invoke sudo if non-root (if not on dev-machine)
if os.geteuid() != 0 and "no_rpio" not in CMD_OPTIONS:
    print("Script not started as root. Running sudo...")
    args = ['sudo', sys.executable] + sys.argv + [os.environ]
    # the next line replaces the currently-running process with the sudo
    os.execlpe('sudo', *args)
    exit(-1)

import curses
import curses.textpad as textpad
import time
from threading import Thread

# This help is displayed inside the program
HELP = """rpio-curses is an interactive RPIO management tool.

You can view and change gpios as well as simply observe them.
rpio-curses automatically updates all gpio functions and states
every second.

Move up and down with the keyboard, and select a gpio which
you want to update with [Enter] or [arrow-right]. To get out
of a menu you can press [arrow-left] or [ESC].

Author: Chris Hager <chris@linuxuser.at>
URL: http://pythonhosted.org/RPIO
License: LGPLv3+
"""

# Timeout between refreshing the GPIO states. You can adjust
# this to your needs (eg. `= 0.5`, ...)
THREAD_TIMEOUT_SECONDS = 1

# Dummy GPIO list to use on dev machines (gpio-id, function, state)
GPIO_LIST = [(i+2, i%3 if i%3 < 2 else 4, i%2) for i in range(20)]

# Main windows options
OPTIONS = ("Help [h]", "Quit [q]")

# Pretty print states and functions
GPIO_FUNCTIONS = {0: "OUTPUT", 1: "INPUT", 4: "ALT0", 7: "-"}
STATES = ["Low", "High"]

# X/Y positioning of 'windows' in curses
POS_GPIOLIST_X = 5
POS_GPIOLIST_Y = 2
POS_GPIOINFO_X = 40
POS_GPIOINFO_Y = 2

# The cursor width depends on the options of this pins function
CURSOR_WIDTHS = {0: 23, 1: 30, 4: 17}

# Only import RPIO if not on a dev machine
RPIO_VERSION = "dev"
if "no_rpio" not in CMD_OPTIONS:
    import RPIO
    RPIO.setwarnings(False)
    RPIO_VERSION = RPIO.VERSION
    def get_gpiolist():
        if RPIO.sysinfo()[1] == 'B+':
          pins = RPIO.GPIO_LIST_R3
        elif RPIO.RPI_REVISION == 1:
          pins = RPIO.GPIO_LIST_R1
        else:
          pins = RPIO.GPIO_LIST_R2

        id_list = list(pins)
        id_list.sort()

        gpios = []
        for gpio_id_str in id_list:
            gpio_id = int(gpio_id_str)
            function = RPIO.gpio_function(gpio_id)
            state = RPIO.forceinput(gpio_id)
            gpios.append((gpio_id, function, 1 if state else 0))

        return gpios
    GPIO_LIST = get_gpiolist()
    HELP += ("\nRaspberry System Info:\n[%s] Model %s, Revision %s, RAM: %s MB, "
            "Maker: %s") % RPIO.sysinfo()

#
# GPIO methods
#
def set_input(gpio_id):
    RPIO.setup(int(gpio_id), RPIO.IN)

def set_output(gpio_id):
    RPIO.setup(gpio_id, RPIO.OUT)

def set_pullup(gpio_id):
    RPIO.set_pullupdn(gpio_id, RPIO.PUD_UP)

def set_pulldown(gpio_id):
    RPIO.set_pullupdn(gpio_id, RPIO.PUD_DOWN)

def set_pulloff(gpio_id):
    RPIO.set_pullupdn(gpio_id, RPIO.PUD_OFF)

def set_high(gpio_id):
    RPIO.forceoutput(gpio_id, RPIO.HIGH)

def set_low(gpio_id):
    RPIO.forceoutput(gpio_id, RPIO.LOW)


class TimedCallback(Thread):
    """ Simple callback after timeout via thread. Used for auto-update """
    is_running = True
    def __init__(self, timeout, cb):
        Thread.__init__(self)
        self.timeout = timeout
        self.cb = cb

    def run(self):
        # parts is a 'trick' to quit faster
        parts = 10
        timeout_part = float(self.timeout) / parts
        for i in range(parts):
            if not self.is_running:
                return
            time.sleep(timeout_part)
        if self.is_running:
            self.cb()


class Drawer(object):
    """ All in one. Does the curses drawing as well as everything else. """
    gpios = GPIO_LIST

    cursor_index = 0
    cursor_index_menu2 = 0
    cursor_menu2_width = 30

    pos_cursor_y = None  # gets set from __init__ to allow adjustments
    pos_cursor_x = POS_GPIOLIST_X - 3

    pos_cursor_menu2_y = POS_GPIOINFO_Y + 5
    pos_cursor_menu2_x = POS_GPIOINFO_X - 1

    thread = None

    len_options_menu2 = 0
    is_menu2 = False
    is_help = False
    is_running = True

    def __init__(self, screen):
        self.screen = screen
        self.pos_cursor_y = POS_GPIOLIST_Y

    def draw(self):
        screen = self.screen
        #screen.clear()
        #screen.refresh()

        screen.addstr(0, 0, "RPIO v%s" % RPIO_VERSION)

        pos_y = POS_GPIOLIST_Y
        for gpio in self.gpios:
            
            gpio_id, func, state = gpio
            screen.addstr(pos_y, POS_GPIOLIST_X, "GPIO %s: " % (gpio_id))
            screen.addstr(pos_y, POS_GPIOLIST_X+9, "%s     " % (GPIO_FUNCTIONS[func]), curses.color_pair(3) if func == 4 else 0)
            screen.addstr(pos_y, POS_GPIOLIST_X+18, "%s    " % STATES[state], curses.color_pair(2) if state else 0)
            pos_y += 1

        for option in OPTIONS:
            pos_y += 1
            screen.addstr(pos_y, POS_GPIOLIST_X, option)

        # Cursor
        self.draw_cursor()

    def draw_cursor(self):
        self.screen.addstr(self.pos_cursor_y, self.pos_cursor_x, ">")
        self.screen.addstr(self.pos_cursor_y, self.pos_cursor_x+27, "<")

    def draw_gpio_info(self, gpio_index):
        gpio, function, state = self.gpios[gpio_index]
        self.gpio_menu2 = gpio
        self.function_menu2 = function
        self.cursor_menu2_width = CURSOR_WIDTHS[function]

        screen = self.screen
        screen.addstr(POS_GPIOINFO_Y, POS_GPIOINFO_X, "GPIO %-20s" % gpio)
        seplen = 12 + len(GPIO_FUNCTIONS[function])
        screen.addstr(POS_GPIOINFO_Y+1, POS_GPIOINFO_X, "%s" % ("-" * seplen))
        screen.addstr(POS_GPIOINFO_Y+2, POS_GPIOINFO_X, "- Function:")
        screen.addstr(POS_GPIOINFO_Y+2, POS_GPIOINFO_X+12, "%-10s" % GPIO_FUNCTIONS[function], curses.color_pair(3) if function == 2 else 0)
        screen.addstr(POS_GPIOINFO_Y+3, POS_GPIOINFO_X, "- State:")
        screen.addstr(POS_GPIOINFO_Y+3, POS_GPIOINFO_X+12, "%-10s" % STATES[state], curses.color_pair(2) if state else 0)

        if function == 1:
            # INPUT
            screen.addstr(POS_GPIOINFO_Y+5, POS_GPIOINFO_X+2, "Set to OUTPUT             ")
            screen.addstr(POS_GPIOINFO_Y+6, POS_GPIOINFO_X+2, "Set resistor to pull-up   ")
            screen.addstr(POS_GPIOINFO_Y+7, POS_GPIOINFO_X+2, "Set resistor to pull-down ")
            screen.addstr(POS_GPIOINFO_Y+8, POS_GPIOINFO_X+2, "Set resistor to off       ")
            screen.addstr(POS_GPIOINFO_Y+9, POS_GPIOINFO_X+2, "back                      ")
            self.len_options_menu2 = 5
        elif function == 0:
            screen.addstr(POS_GPIOINFO_Y+5, POS_GPIOINFO_X+2, "Set to INPUT              ")
            screen.addstr(POS_GPIOINFO_Y+6, POS_GPIOINFO_X+2, "Set output to High        ")
            screen.addstr(POS_GPIOINFO_Y+7, POS_GPIOINFO_X+2, "Set output to Low         ")
            screen.addstr(POS_GPIOINFO_Y+8, POS_GPIOINFO_X+2, "back                      ")
            screen.addstr(POS_GPIOINFO_Y+9, POS_GPIOINFO_X+2, "                          ")
            self.len_options_menu2 = 4
        elif function == 4:
            screen.addstr(POS_GPIOINFO_Y+5, POS_GPIOINFO_X+2, "Set to INPUT              ")
            screen.addstr(POS_GPIOINFO_Y+6, POS_GPIOINFO_X+2, "Set to OUTPUT             ")
            screen.addstr(POS_GPIOINFO_Y+7, POS_GPIOINFO_X+2, "back                      ")
            screen.addstr(POS_GPIOINFO_Y+8, POS_GPIOINFO_X+2, "                          ")
            screen.addstr(POS_GPIOINFO_Y+9, POS_GPIOINFO_X+2, "                          ")
            self.len_options_menu2 = 3

        self.pos_cursor_menu2_y = self.cursor_index_menu2 + POS_GPIOINFO_Y + 5
        screen.addstr(self.pos_cursor_menu2_y, self.pos_cursor_menu2_x, ">")
        screen.addstr(self.pos_cursor_menu2_y, self.pos_cursor_menu2_x+self.cursor_menu2_width, "<")


    def menu2_clear(self):
        self.screen.addstr(self.pos_cursor_menu2_y, self.pos_cursor_menu2_x, " ")
        self.screen.addstr(self.pos_cursor_menu2_y, self.pos_cursor_menu2_x +self.cursor_menu2_width, " ")
        for i in range(12):
            self.screen.addstr(POS_GPIOINFO_Y+i, POS_GPIOINFO_X, "%30s" % "")

    def menu2_move_cursor(self, up=True):
        offset = -1 if up else 1
        py = self.cursor_index_menu2 + offset
        _py = py

        if py < 0:
            py = self.len_options_menu2 - 1

        elif py == self.len_options_menu2:
            py = 0

        self.screen.addstr(self.pos_cursor_menu2_y, self.pos_cursor_menu2_x, " ")
        self.screen.addstr(self.pos_cursor_menu2_y, self.pos_cursor_menu2_x +self.cursor_menu2_width, " ")

        self.cursor_index_menu2 = py
        self.pos_cursor_menu2_y = self.cursor_index_menu2 + POS_GPIOINFO_Y + 5

        self.screen.addstr(self.pos_cursor_menu2_y, self.pos_cursor_menu2_x, ">")
        self.screen.addstr(self.pos_cursor_menu2_y, self.pos_cursor_menu2_x+self.cursor_menu2_width, "<")

    def move_cursor(self, up=True):
        self.help_clear()
        if self.is_menu2:
            return self.menu2_move_cursor(up)

        offset = -1 if up else 1
        py = self.cursor_index + offset
        _py = py

        if py < 0:
            py = len(self.gpios) + len(OPTIONS)

        elif py == len(self.gpios):
            py += -1 if up else 1

        elif py == len(self.gpios) + len(OPTIONS) + 1:
            py = 0

        self.screen.addstr(self.pos_cursor_y, self.pos_cursor_x, " ")
        self.screen.addstr(self.pos_cursor_y, self.pos_cursor_x+27, " ")

        self.cursor_index = py
        self.pos_cursor_y = self.cursor_index + POS_GPIOLIST_Y

        self.draw_cursor()

    def show_help(self):
        self.back_to_menu1()
        lines = HELP.split("\n")
        i = 0
        for line in lines:
            self.screen.addstr(POS_GPIOINFO_Y+i, POS_GPIOINFO_X, line)
            i += 1
        self.is_help = True

    def help_clear(self):
        if not self.is_help:
            return
        i = 0
        for i in range(len(HELP.split("\n"))):
            self.screen.addstr(POS_GPIOINFO_Y+i, POS_GPIOINFO_X, "%63s" % "")
        self.is_help = False

    def enter(self):
        #self.log("enter on index %s  " % self.cursor_index)
        if not self.is_menu2:
            if self.cursor_index < len(self.gpios):
                #self.log("gpio %s selected" % self.cursor_index)
                self.go_to_menu2()

            else:
                option_id = self.cursor_index - len(self.gpios) - 1
                #self.log("option %s          " % option_id)
                if option_id == 0:
                    self.show_help()
                elif option_id == 1:
                    self.quit()

        else:
            # gpio info menu
            option_id = self.cursor_index_menu2
            #self.log("option2: %-20s" % option_id)
            if self.function_menu2 == 1:
                # Input GPIO
                if option_id == 0:
                    set_output(self.gpio_menu2)
                    self._refresh(start_thread=False)
                elif option_id == 1:
                    set_pullup(self.gpio_menu2)
                    self._refresh(start_thread=False)
                elif option_id == 2:
                    set_pulldown(self.gpio_menu2)
                    self._refresh(start_thread=False)
                elif option_id == 3:
                    set_pulloff(self.gpio_menu2)
                    self._refresh(start_thread=False)
                elif option_id == 4:
                    self.back_to_menu1()

            elif self.function_menu2 == 0:
                # OUTPUT
                if option_id == 0:
                    set_input(self.gpio_menu2)
                    self._refresh(start_thread=False)
                elif option_id == 1:
                    set_high(self.gpio_menu2)
                    self._refresh(start_thread=False)
                elif option_id == 2:
                    set_low(self.gpio_menu2)
                    self._refresh(start_thread=False)
                elif option_id == 3:
                    self.back_to_menu1()

            elif self.function_menu2 == 4:
                # ALT0
                if option_id == 0:
                    set_input(self.gpio_menu2)
                    self._refresh(start_thread=False)
                elif option_id == 1:
                    set_output(self.gpio_menu2)
                    self._refresh(start_thread=False)
                elif option_id == 2:
                    self.back_to_menu1()

    def go_to_menu2(self, reset_option_index=True):
        self.help_clear()

        # remove old cursor
        self.screen.addstr(self.pos_cursor_y, self.pos_cursor_x, " ")
        self.screen.addstr(self.pos_cursor_y, self.pos_cursor_x+27, " ")

        # enter menu2
        self.is_menu2 = True
        if reset_option_index:
            self.cursor_index_menu2 = 0
        self.draw_gpio_info(self.cursor_index)

    def back_to_menu1(self):
        if self.is_menu2:
            self.menu2_clear()
            self.draw_cursor()
            self.is_menu2 = False

    def left(self):
        if self.is_menu2:
            self.back_to_menu1()

    def right(self):
        if not self.is_menu2:
            self.go_to_menu2()

    def log(self, msg):
        self.screen.addstr(1, 20, "%-20s" % msg)

    def _refresh(self, start_thread=True):
        """ Started by Thread, to refresh everything """
        if not self.is_running:
            return

        self.gpios = get_gpiolist()
        self.screen.refresh()
        if self.is_menu2:
            self.back_to_menu1()
            self.draw()
            self.go_to_menu2(reset_option_index=False)
        else:
            self.draw()

        if start_thread:
            self.start_thread()

    def start_thread(self, timeout=1):
        self.thread = None
        self.thread = TimedCallback(timeout, self._refresh)
        self.thread.start()

    def start(self):
        self.draw()
        if "no_thread" not in CMD_OPTIONS:
            self.start_thread()

    def quit(self, retval=0):
        self.is_running = False
        if self.thread:
            self.thread.is_running = False
        #time.sleep(2)
        curses.nocbreak(); self.screen.keypad(0); curses.echo()
        curses.endwin()
        exit(retval)


def main():
    exit_msg = ""
    try:
        screen = curses.initscr()

        # Check that the screen size is within minimum limits
        min_h, min_w = (len(GPIO_LIST) + 5, 70)
        win_h, win_w = screen.getmaxyx()
        if win_w > min_w and win_h == min_h - 1:
            # We can cut one line
            global POS_GPIOLIST_Y
            POS_GPIOLIST_Y -= 1
        elif win_h < min_h or win_w < min_w:
            exit_msg = ("Your terminal window is too small. `rpio-curses` "
                    "needs %s lines and 70 columns (you have %s lines, %s "
                    "columns).") % (len(GPIO_LIST) + 5, win_h, win_w)
            exit(-1)

        # Start the fun
        curses.cbreak();
        curses.noecho()
        screen.keypad(1);
        curses.curs_set(0)
        curses.start_color()
        curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
        curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
        curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)

        # Our dear all-in-one magic class
        d = Drawer(screen)
        d.start()

        # And the event loop
        while True:
            event = screen.getch()
            if event == ord("q"):
                d.quit()
            elif event == 259:
                d.move_cursor(up=True)
            elif event == 258:
                d.move_cursor(up=False)
            elif event == 10:
                d.enter()
            elif event == 260 or event == 27:
                d.left()  # 27=ESC
            elif event == 261:
                d.right()
            elif event == 104:
                d.show_help()
            else:
                #screen.addstr(0, 100, "Pressed: %-10s" % event)
                pass

    finally:
        # Any way, this is gonna happen at the end of the program
        curses.nocbreak()
        screen.keypad(0)
        curses.echo()
        curses.endwin()
        if exit_msg:
            print(exit_msg)


if __name__ == "__main__":
    main()
